﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Text;
using System.Xml;
using Microsoft.Win32;

namespace Icedream.InstantFileTransfer
{
    public class IFTPServer : IIFTPPluginInterface
    {
        public const int StandardPort = 28910;
        public const int kB = 1024;
        public const int MB = 1024 * kB;
        public const int GB = 1024 * MB;

        private int _port = IFTPServer.StandardPort;
        private int _bufferSize = 4 * kB;
        private bool _tryPassive = true;
        private bool _doTestTransfer = true;
        private long _testSize = 4 * 1024 * 1024;
        private Thread _serverThr;
        private List<IFTPListFile> _files = new List<IFTPListFile>();
        private XmlDocument _mimeXml;
        private TcpListener _tcp;
        private Dictionary<string, string> _fileCodeCache = new Dictionary<string, string>();
        internal XmlNode _mimeList { get { return _mimeXml.SelectSingleNode("child::mimelist"); } }
       
        #region Properties

        public bool TryPassiveConnection
        { get { return _tryPassive; } set { _tryPassive = value; } }

        public bool DoTestBeforeTransfer
        { get { return _doTestTransfer; } set { _doTestTransfer = value; } }

        public int Port
        { get { return _port; } set { _port = value; } }

        public Version ServerVersion
        { get { return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; } }

        public IFTPListFile[] Files
        { get { return _files.ToArray(); } }

        public int BufferSize
        { get { return _bufferSize; } }

        public long TestSize
        { get { return _testSize; } }
        #endregion

        #region Constructor
        private void __CONSTRUCT()
        {
            _mimeXml = new XmlDocument();
            _mimeXml.Load("MimeList.xml");
        }
        public IFTPServer() {
            __CONSTRUCT();
        }
        public IFTPServer(int port) {
            this._port = port;
            __CONSTRUCT();
        }
        public IFTPServer(int port, bool tryPassive) {
            this._tryPassive = tryPassive;
            this._port = port;
            __CONSTRUCT();
        }
        #endregion

        #region Events
        #endregion

        #region Event Wrappers
        #endregion

        #region Exception generators
        #endregion

        #region Public methods
        public void Start()
        {
            
            Stop();
            _tcp = new TcpListener(IPAddress.Any, _port);
            _serverThr = new Thread(new ThreadStart(Listen));
            Log("Server initialized.");
            _serverThr.Start();
        }

        public void Stop()
        {
            if (_tcp != null)
            {
                _tcp.Stop();
                _tcp = null;
            }
            if (_serverThr != null)
                while (_serverThr.IsAlive)
                    Thread.Sleep(50);
            Log("Server stopped.");
        }

        public void Log(string text)
        {
            Console.WriteLine("[IFTPServer] " + text);
        }
        #endregion

        #region Private methods
        private void HandleClient(object o)
        {
            TcpClient cl = o as TcpClient;

            NetworkStream ns = cl.GetStream();
            StreamReader sr = new StreamReader(ns);
            StreamWriter sw = new StreamWriter(ns);
            IFTPReply reply = null;
            IFTPListFile file = null;
            sw.AutoFlush = true;
            //TimeSpan testduration = TimeSpan.MinValue;
            int _passive = -1;
            string prefix = "[" + ((IPEndPoint)cl.Client.RemoteEndPoint).ToString() + "] ";

            try
            {
                Log(prefix + "Waiting for START.");
                while ((reply = sr.ReadReply())._repName != "START")
                {
                    if (reply._repName == "PASSIVE")
                        int.TryParse(reply._repArgs.Last(), out _passive);
                }

                if (_passive < 0x0001 || _passive > 0xFFFF)
                    _passive = -1;

                Log(prefix + "Waiting for VERSION.");
                if (GetClientVersion(sw, sr) < ServerVersion)
                {
                    NoAcknowledge(sw, "Client version too old");
                    sw.Close();
                    return;
                }

                Log(prefix + "Connection accepted.");
                sw.WriteLine("ACK");

                while (cl.Connected)
                {
                    reply = sr.ReadReply();
                    Log(prefix + "Received " + reply._repName + ".");

                    switch (reply._repName)
                    {
                        case "REQUESTTEST":
                            SendTestFile(sw, sr, _testSize);
                            break;

                        case "CODE":
                            Log(prefix + "Received request for file code " + reply._repArgs.Last());
                            file = GetFileFromCode(reply._repArgs.Last());
                            if (file == null)
                                Acknowledge(sw);
                            else
                                NoAcknowledge(sw);
                            break;

                        case "PASSIVE":
                            int.TryParse(reply._repArgs.Last(), out _passive);
                            if (_passive < 0x0001 || _passive > 0xFFFF)
                                _passive = -1;
                            break;

                        case "INFO":
                            if (file == null)
                            {
                                NoAcknowledge(sw, "No file requested");
                                break;
                            }
                            SendFileInfo(sw, file);
                            Log(prefix + "Sent file info.");
                            break;

                        case "FILE":
                            // TODO [IFTPServer.HandleClient] Implement FILE
                            // TODO [IFTPServer.HandleClient] Implement passive FILE
                            throw new NotImplementedException("FILE is planned to be implemented.");
                            break;

                        case "LIST":
                            Log(prefix + "Sent file list.");
                            SendList(sw);
                            break;

                        case "VERSION":
                            Log(prefix + "Sent version.");
                            sw.WriteLine("VERSION " + ServerVersion.ToString());
                            break;

                        case "QUIT":
                            Log(prefix + "Client requested to quit connection.");
                            ns.Close();
                            Log(prefix + "Client disconnected by server.");
                            return;
                    }
                }
            }
            catch (TimeoutException)
            {
                Log(prefix + "Client timeout.");
            }
            catch (SocketException)
            {
                Log(prefix + "Client socket error.");
            }
            Log(prefix + "Client disconnected itself.");
        }
        private void Listen()
        {
            _tcp.Start();
            Log("Server started.");
            try
            {
                while (!_tcp.Pending())
                    Thread.Sleep(50);
                Log("Incoming connection detected.");
                TcpClient cl = _tcp.AcceptTcpClient();
                Thread thr = new Thread(new ParameterizedThreadStart(HandleClient));
                thr.IsBackground = true;
                thr.Start(cl);
            }
            catch (ThreadStateException)
            {
                return; // TODO [IFTPServer.Listen] Handle ThreadStateException
            }
            catch (SocketException)
            {
                return; // TODO [IFTPServer.Listen] Handle ScoketException
            }
            catch (ObjectDisposedException)
            {
                return;
            }
        }
        #endregion

        #region Commands
        internal void Acknowledge(StreamWriter w)
        { w.WriteLine("ACK"); }
        internal void Acknowledge(StreamWriter w, bool ack)
        { w.WriteLine((!ack ? "N" : "") + "ACK"); }
        internal void Acknowledge(StreamWriter w, bool ack, string reason)
        { w.WriteLine((!ack ? "N" : "") + "ACK \"" + reason + "\""); }
        internal void NoAcknowledge(StreamWriter w)
        { Acknowledge(w, false); }
        internal void NoAcknowledge(StreamWriter w, string reason)
        { Acknowledge(w, false, reason); }
        internal Version GetClientVersion(StreamWriter w, StreamReader r)
        {
            w.WriteLine("VERSION");
            IFTPReply rpl = r.ReadReply();
            return Version.Parse(rpl._repArgs.Last()); }
        internal TimeSpan GetTestTime(StreamWriter w, StreamReader r)
        {
            w.WriteLine("TESTTIME");
            IFTPReply rpl = r.ReadReply();
            TimeSpan time = TimeSpan.Parse(rpl._repArgs[0]);
            return time;
        }
        internal int GetPassivePort(StreamWriter w, StreamReader r)
        { w.WriteLine("PASSIVE"); IFTPReply rpl = r.ReadReply(); if (rpl._repName == "NACK") return -1; else return int.Parse(rpl._repArgs.Last()); }
        internal void SendList(StreamWriter w)
        { w.WriteLine("LIST"); foreach (IFTPListFile file in _files) { w.WriteLine("LFILE \"" + file._name + "\" " + file._size.ToString() + " " + file._code); } w.WriteLine("EOLIST"); }
        internal void SendFileInfo(StreamWriter w, IFTPListFile file)
        { SendFileInfo(w, file._name); }
        internal void SendFileInfo(StreamWriter w, string file)
        {
            w.WriteLine("INFO");
            FileInfo fi = new FileInfo(file);
            w.WriteLine("Name: " + fi.Name);
            w.WriteLine("Size: " + fi.Length.ToString());
            w.WriteLine("CreationTime: " + fi.CreationTime.ToString());
            w.WriteLine("CreationTimeUtc: " + fi.CreationTimeUtc.ToString());
            w.WriteLine(fi.Extension);
            w.WriteLine("LastWriteTime: " + fi.LastWriteTime.ToString());
            w.WriteLine("LastWriteTimeUtc: " + fi.LastWriteTimeUtc.ToString());
            w.WriteLine("EOINFO");
        }
        internal bool SendFile(StreamWriter w, StreamReader r, string file)
        {
            IFTPStreamPlugin plugin = null;

            w.WriteLine("BUFFER " + _bufferSize);

            foreach (IFTPStreamPlugin p in _streamPlugins)
            {
                if (p.CanCompress && p.CompressionString != null)
                {
                    plugin = p;
                    w.WriteLine("FILE " + plugin.CompressionString);
                    if (r.ReadAckReply().IsAcknowledged)
                        break;
                    else
                    {
                        plugin = null;
                    }
                }
            }

            if (plugin == null)
            {
                w.WriteLine("NACK");
                return false;
            }

            FileStream fs = File.OpenRead(file);

            return plugin.Compress(fs, w.BaseStream as NetworkStream, _bufferSize, GetFileCodeFromCache(file));
        }
        internal string GetFileCodeFromCache(string file)
        {
            FileInfo fi = new FileInfo(file);
            return _fileCodeCache[fi.FullName.ToLower()];
        }
        internal bool SendTestFile(StreamWriter w, StreamReader r, long size)
        {
            Random rand = new Random();
            w.WriteLine("TEST " + size);
            if (!r.ReadAckReply().IsAcknowledged)
                return false;
            byte[] buffer = new byte[4096];
            for (long i = 0; i < size; i += buffer.Length)
            {
                if (size - i < 4096) buffer = new byte[size - i];
                rand.NextBytes(buffer);
                w.BaseStream.Write(buffer, 0, buffer.Length);
            }
            w.Flush();
            return true;
        }
        internal IFTPListFile GetFileFromCode(string code)
        {
            foreach (IFTPListFile file in _files)
            {
                if (file._code == code)
                {
                    return file;
                }
            }
            return null;
        }
        #endregion

        #region MIME functions
        /// <summary>
        /// Fetches the mime type by using the included MimeList.xml.
        /// </summary>
        /// <param name="extension">The file extension to search for</param>
        /// <returns>The MIME type found. If none is found, this is "application/octet-stream".</returns>
        internal string GetMime(string extension)
        {
            XmlNode n = _mimeList.SelectSingleNode("child::mime[ext=\"" + extension + "\"]");
            if (n == null)
                try
                {
                    return GetMimeFromRegistry(extension);
                }
                catch (PlatformNotSupportedException)
                {
                    return "application/octet-stream";
                }
            return n.InnerText;
        }

        /// <summary>
        /// Fetches the mime type from the Windows Registry.
        /// </summary>
        /// <param name="extension">The file extension to search for</param>
        /// <returns>The MIME type found. If none is found, this is "application/octet-stream".</returns>
        /// <exception cref="PlatformNotSupportedException">Thrown if system is not a Windows platform</exception>
        internal string GetMimeFromRegistry(string extension)
        {
            extension = extension.Replace(".", "");
            if (!Environment.OSVersion.Platform.ToString().Contains("Win"))
                throw new PlatformNotSupportedException("Mime by extension only available on Windows platforms.");
            string mimeType = "application/octet-stream";
            RegistryKey extensionKey = Registry.ClassesRoot.OpenSubKey("." + extension);
            if (extensionKey == null)
                return mimeType;
            object contentType = extensionKey.GetValue("ContentType", null);
            if (contentType == null)
                return mimeType;
            mimeType = contentType.ToString();
            return mimeType;
        }
        #endregion
    }
}
